//From 2020-4-13, it will be compiled as unzip-go.
//The program can unzip a hole zip-archive now.
//It alse can list files in a zip-archive, or unzip one file from a zip-archive.
//Author: Fu Huizhong<fuhuizn@163.com>
package main

import (
	"flag"
	"fmt"
	"github.com/yeka/zip"
	"io"
	"os"
	"path"
	"path/filepath"

	"golang.org/x/text/encoding"
	"golang.org/x/text/encoding/simplifiedchinese"
)

var decoder = simplifiedchinese.GB18030.NewDecoder()

func getName(v *zip.File) (name string, err error) {

	tmp := make([]byte, len([]byte(v.Name)))
	_, _, err = encoding.UTF8Validator.Transform(tmp, []byte(v.Name), true)
	if err == nil {
		name = v.Name
	} else {
		name, err = decoder.String(v.Name)
		if err != nil {
			return
		}
	}

	return
}

func main() {
	var h = flag.Bool("h", false, "显示帮助")
	var l = flag.Bool("l", false, "列出所有文件的序号")
	var idx = flag.Int("idx", -1, "要提取的文件的序号，用zipls查看。默认解压压缩包中的所有文件。")
	var d = flag.String("d", "", "解压到该目录")
	flag.Parse()
	if *h {
		fmt.Fprintf(os.Stderr, "格式：%v -idx <序号> <zip文件>\n\n", os.Args[0])
		flag.Usage()
		os.Exit(1)
	}
	fp, err := zip.OpenReader(flag.Arg(0))
	if err != nil {
		panic(err)
	}
	defer fp.Close()
	if len(*d) > 0 {
		os.Chdir(*d)
	} else {
		os.Chdir(filepath.Dir(flag.Arg(0)))
	}

	if *l {
		ListZip(fp)
		return
	}

	var name string

	if *idx < 0 || *idx >= len(fp.File) {
	    //解压缩整个文件
		err = UnzipAll(fp, filepath.Base(flag.Arg(0)))
		if err != nil {
			fmt.Println(err)
		}
		return
	}
    //提取单个文件
	v := fp.File[*idx]

	name, err = getName(v)
	if err != nil {
		panic(err)
	}

	if v.FileInfo().IsDir() {
		panic("该路径是一个目录")
	}
	var pwd string
	if v.IsEncrypted() && len(pwd)==0 {
	    fmt.Print("Password:\n")
	    fmt.Scanln(&pwd)
	    v.SetPassword(pwd)
	}
	fmt.Println(*idx, v.Mode(), name, v.UncompressedSize64)
	fpr, err := v.Open()
	if err != nil {
		panic(err)
	}
	defer fpr.Close()
	fpw, err := os.Create(path.Base(name))
	if err != nil {
		panic(err)
	}
	io.Copy(fpw, fpr)
	fpw.Close()
	os.Chtimes(path.Base(name), v.ModTime(), v.ModTime())
}

func UnzipAll(fp *zip.ReadCloser, fname string) error {

	bname := []byte(fname)
	rname := string(bname[:len(bname)-4])

	os.MkdirAll(rname, os.ModePerm)
	os.Chdir(rname)
	var name string
	var err error
	var pwd string
	for _, v := range fp.File {
		name, err = getName(v)
		if err != nil {
			return err
		}
		if v.FileInfo().IsDir() {
			os.MkdirAll(name, os.ModePerm)
			continue
		}
		if v.IsEncrypted() && len(pwd)==0 {
		    fmt.Print("Password:\n")
		    fmt.Scanln(&pwd)
		}
		dir1 := filepath.Dir(name)
		os.MkdirAll(dir1, os.ModePerm)
		//fmt.Println(i, v.Mode(), name, v.UncompressedSize64)
	}
	var i int = 1
	for _, v := range fp.File {
		if v.FileInfo().IsDir() {
			continue
		}
		if v.IsEncrypted() {
		    v.SetPassword(pwd)
		}
		name, err = getName(v)
		if err != nil {
			return err
		}
		fpr, err := v.Open()
		if err != nil {
			return err
		}
		fpw, err := os.Create(name)
		if err != nil {
			fpr.Close()
			return err
		}
		io.Copy(fpw, fpr)
		fpw.Close()
		fpr.Close()
		os.Chtimes(name, v.ModTime(), v.ModTime())
		os.Chmod(name, v.Mode())
		fmt.Println(i, v.Mode(), name, v.UncompressedSize64)
		i++
	}
	return nil
}

func ListZip(fp *zip.ReadCloser) error {

	var name string
	var err error
	for i, v := range fp.File {
		if v.FileInfo().IsDir() {
			continue
		}
		name, err = getName(v)
		if err != nil {
			return err
		}

		fmt.Printf("%v\t%v\t%v\n", i, v.Mode(), name)
	}
	return nil
}
